#include "simodo/lsp-client/LanguageClient.h"

#include <boost/asio.hpp>
#ifdef CROSS_WIN
#include <boost/process/windows.hpp>
#endif

namespace simodo::lsp
{
using namespace simodo::inout;

inline const std::string content_length_const = "Content-Length: ";

LanguageClient::LanguageClient(std::string process_path, std::vector<std::string> process_args, 
                variable::Object initialize_params, inout::Logger_interface & logger,
                ServerRestartSupport_interface * )
    : _process_path(process_path)
    , _process_args(process_args)
    , _initialize_params(initialize_params)
    , _log(logger)
    // , _server_restart_support(server_restart_support)
{
    _ok = start();

    if (!_ok)
        close();
}

LanguageClient::~LanguageClient()
{
    close();
    if (_sending_thread)
        _sending_thread->join();
    if (_response_thread)
        _response_thread->join();
}

InitializeResult LanguageClient::initialize_result() const
{ 
    std::lock_guard locker(_initialize_result_mutex);
    return _initialize_result; 
}

ServerCapabilities LanguageClient::server_capabilities() const
{ 
    std::lock_guard locker(_initialize_result_mutex);
    return _initialize_result.capabilities; 
}

SimodoCapabilities LanguageClient::simodo_capabilities() const
{
    std::lock_guard locker(_initialize_result_mutex);
    return _initialize_result.simodo; 
}

int64_t LanguageClient::exec(const std::string method, variable::Value params, RpcHandle handle, const void * sender)
{
    std::unique_ptr<variable::JsonRpc> rpc;

    if (handle)
        rpc = std::make_unique<variable::JsonRpc>(toU16(method), params, ++_last_id);
    else
        rpc = std::make_unique<variable::JsonRpc>(toU16(method), params);

    return exec(*rpc, handle, sender);
}

bool LanguageClient::done(uint64_t timeout_mills)
{
    if (!running())
        return true;

    uint64_t elapsed_mills = 0;

    while(elapsed_mills < timeout_mills) {
        if (_queue_for_sending.empty()) {
            std::this_thread::sleep_for(std::chrono::milliseconds(500));
            if (_queue_for_sending.empty())
                return true;
        }
        else
            std::this_thread::sleep_for(std::chrono::milliseconds(500));

        elapsed_mills += 500;
    }

    return false;
}

void LanguageClient::close()
{
    if (_stopping)
        return;

    if (_language_server) {
        if (waitResponse(exec("shutdown", {}, [](variable::JsonRpc, const void *){})))
            exec("exit", {});

        _stopping = true;

        std::error_code ec;
        // @todo Перейти на boost::process::v2
        // _language_server->wait_for(std::chrono::milliseconds(100), ec);
        // if (ec)
        //     _log.error("Language server process stopping error", ec.message());
        std::this_thread::sleep_for(std::chrono::milliseconds(100));

        if (_language_server->running(ec))
        {
            _language_server->terminate(ec);
        }
        _language_server->wait(ec);
    }
}

bool LanguageClient::registerListener(std::string method, RpcHandle listener)
{
    std::lock_guard locker(_commands_listeners_mutex);
    _commands_listeners[method] = listener;
    return true;
}

// ------------------------------------------------------------------------------------------
// private ----------------------------------------------------------------------------------
// ------------------------------------------------------------------------------------------

bool LanguageClient::start()
{
    bool ok = true;

    try {
        _language_server = std::make_unique<bp::child>(
                              _process_path
                            , _process_args
                            , bp::std_in < _os
                            , bp::std_out > _is
#ifdef CROSS_WIN
                            , bp::windows::hide
#endif
                            , bp::on_exit([&](auto...)
                            {
                                _is.close();
                                _os.close();
                                _log.info("Server stoped");
                                _ok = false;
                            }
                            ));

        if (!_language_server->running()) {
            _log.critical("Unable to run language server (stop)");
            std::error_code ec;
            _language_server->terminate(ec);
            _language_server.reset();
            ok = false;
        }
    }
    catch(const std::exception& e) {
        _log.critical("Unable to run language server (stop)", e.what());
        _language_server.reset();
        ok = false;
    }
    
    if (ok) {
        _sending_thread  = std::make_unique<std::thread>(& LanguageClient::sender, this);
        _response_thread = std::make_unique<std::thread>(& LanguageClient::response_listener, this);

        int64_t id = exec("initialize", _initialize_params, 
                          [this](variable::JsonRpc rpc, const void *){
                            if (!rpc.is_valid()) {
                                _log.critical("Initialize result error", rpc.origin());
                                return;
                            }
                            std::lock_guard locker(_initialize_result_mutex);
                            _initialize_result = parseInitializeResult(rpc.result());
                          });

        if (id > 0) {
            if (waitResponse(id))
                exec("initialized", {});
            else {
                _log.critical("Response timeout occurred (stop)");
                ok = false;
            }
        }
        else {
            _log.critical("Response sending error occurred (stop)");
            ok = false;
        }
    }

    return ok;
}

int64_t LanguageClient::exec(const variable::JsonRpc & rpc, RpcHandle handle, const void * sender)
{
    if (!_os.good() || !rpc.is_valid()) 
        return -1;

    int64_t id = rpc.id();

    if ((id > 0 && handle == nullptr) || (id == -1 && handle != nullptr))
        return -1;

    std::string json = variable::toJson(rpc.value(), true, true);
    _queue_for_sending.push(json);

    if (id > 0) {
        std::lock_guard locker(_commands_waiting_to_be_executed_mutex);
        _commands_waiting_to_be_executed.insert({id, {handle,sender}});
    }
    
    return id;
}

void LanguageClient::sender()
try
{
    _log.debug("Output pipe sender starting");
    while(_ok && !_stopping) {
        std::string json;
        bool        got = _queue_for_sending.pop_or_timeout(json,100);

        if (got) {
            if (_os.good()) {
                _os << content_length_const << json.length() 
                    << "\n\n"
                    << json;
                _os.flush();
            }
            else {
                _log.critical("Bad pipe state has occurred during send operation");
                _ok = false;
                break;
            }
        }
    }

    _log.debug("Output pipe sender ending");
}
catch(const std::exception & e)
{
    _log.critical("Exception has occurred during send operation", e.what());
    _ok = false;
}
catch(...)
{
    _log.critical("Exception has occurred during send operation");
    _ok = false;
}

void LanguageClient::response_listener()
{
    _log.debug("Input pipe listener starting");
    while(_ok && !_stopping) {
        std::string line;
        size_t      content_length = 0;
        auto is_empty = [](const std::string & line) -> bool
        {
            return line.empty()
                || (
                    line.size() == 1
                    && line.back() == '\r'
                );
        };
        while(_is.good() && std::getline(_is,line) && !is_empty(line)) {
            if (line.find(content_length_const) == 0)
                content_length = std::stol(line.substr(content_length_const.size()));
        }

        if (!_is.good()) {
            // Штатная ситуация
            // _log.info("Wrong input pipe state (stop listening)");
            _ok = false;
            break;
        }
        _log.debug("Ready receive content length " + std::to_string(content_length));

        std::string content;
        for(size_t i=0; i < content_length && _is.good(); ++i)
            content += _is.get();

        _log.debug("Ready work with content", content);

        if (content.length() != content_length || !verifyJson(content)) {
            _log.error("Wrong JSON-RPC content (try to recover input pipe)", content);
            continue;
        }

        RpcHandle                 handle = nullptr;
        const void *              sender = nullptr;
        const variable::JsonRpc rpc(content);
        if (!rpc.is_valid()) {
            _log.error("JSON-RPC structure is invalid", content);
        }
        else if (!rpc.method().empty()) {
            {
                std::lock_guard locker(_commands_listeners_mutex);
                auto            it = _commands_listeners.find(toU8(rpc.method()));
                if (it == _commands_listeners.end())
                    _log.warning("An command or notification was ignored", content);
                else
                    handle = it->second;
            }
            if (handle)
                handle(rpc, nullptr);
        }
        else {
            {
                std::lock_guard locker(_commands_waiting_to_be_executed_mutex);
                auto            it  = _commands_waiting_to_be_executed.find(rpc.id());
                if (it != _commands_waiting_to_be_executed.end()) {
                    handle = it->second.first;
                    sender = it->second.second;
                    _commands_waiting_to_be_executed.erase(it);
                }
                else
                    _log.error("An unexpected response was received", content);
            }
            if (handle) {
                handle(rpc, sender);
                _command_complete_condition.notify_one();
            }
        }
    }
    _log.debug("Input pipe listener ending");
}

bool LanguageClient::waitResponse(int64_t id, int timeout_mills)
{
    if (id <= 0)
        return false;

    std::unique_lock locker(_command_complete_condition_mutex);
    return _command_complete_condition.wait_for(locker, std::chrono::milliseconds(timeout_mills), [&] { 
            std::lock_guard locker(_commands_waiting_to_be_executed_mutex);
            return _commands_waiting_to_be_executed.find(id) == _commands_waiting_to_be_executed.end();
        });
}

bool LanguageClient::verifyJson(const std::string & str)
{
    return !str.empty() && str[0] == '{' && str[str.size()-1] == '}';
}

InitializeResult LanguageClient::parseInitializeResult(const variable::Value & result_value)
{
    InitializeResult res;

    if (result_value.type() != variable::ValueType::Object) {
        _log.error("LanguageClient::parseInitializeResult: unable to parse result 1");
        return res;
    }

    const variable::Value & capabilities_value  = result_value.getObject()->find(u"capabilities");
    const variable::Value & simodo_value        = result_value.getObject()->find(u"simodo");
    const variable::Value & serverInfo_value    = result_value.getObject()->find(u"serverInfo");

    if (capabilities_value.type() != variable::ValueType::Object) {
        _log.error("LanguageClient::parseInitializeResult: unable to parse result 2");
        return res;
    }

    const variable::Value & positionEncoding_value  = capabilities_value.getObject()->find(u"positionEncoding");
    const variable::Value & textDocumentSync_value  = capabilities_value.getObject()->find(u"textDocumentSync");
    const variable::Value & hoverProvider_value     = capabilities_value.getObject()->find(u"hoverProvider");
    const variable::Value & declarationProvider_value=capabilities_value.getObject()->find(u"declarationProvider");
    const variable::Value & definitionProvider_value= capabilities_value.getObject()->find(u"definitionProvider");
    const variable::Value & referencesProvider_value= capabilities_value.getObject()->find(u"referencesProvider");
    const variable::Value & documentSymbolProvider_value = capabilities_value.getObject()->find(u"documentSymbolProvider");
    const variable::Value & semanticTokensProvider_value = capabilities_value.getObject()->find(u"semanticTokensProvider");
    const variable::Value & completionProvider_value = capabilities_value.getObject()->find(u"completionProvider");

    if (positionEncoding_value.type() == variable::ValueType::String)
        res.capabilities.positionEncoding = positionEncoding_value.getString();

    if (textDocumentSync_value.type() == variable::ValueType::Object) {
        const variable::Value & openClose_value = textDocumentSync_value.getObject()->find(u"openClose");
        const variable::Value & change_value    = textDocumentSync_value.getObject()->find(u"change");

        if (openClose_value.type() == variable::ValueType::Bool)
            res.capabilities.textDocumentSync.openClose = openClose_value.getBool();
        if (change_value.type() == variable::ValueType::Int)
            res.capabilities.textDocumentSync.change = static_cast<TextDocumentSyncKind>(change_value.getInt());
    }

    if (hoverProvider_value.type() == variable::ValueType::Bool)
        res.capabilities.hoverProvider = hoverProvider_value.getBool();
    if (declarationProvider_value.type() == variable::ValueType::Bool)
        res.capabilities.declarationProvider = declarationProvider_value.getBool();
    if (definitionProvider_value.type() == variable::ValueType::Bool)
        res.capabilities.definitionProvider = definitionProvider_value.getBool();
    if (referencesProvider_value.type() == variable::ValueType::Bool)
        res.capabilities.referencesProvider = referencesProvider_value.getBool();
    if (documentSymbolProvider_value.type() == variable::ValueType::Bool)
        res.capabilities.documentSymbolProvider = documentSymbolProvider_value.getBool();

    if (semanticTokensProvider_value.type() == variable::ValueType::Object) {
        const variable::Value & legend_value = semanticTokensProvider_value.getObject()->find(u"legend");
        if (legend_value.type() == variable::ValueType::Object) {
            const variable::Value & tokenTypes_value = legend_value.getObject()->find(u"tokenTypes");
            const variable::Value & tokenModifiers_value = legend_value.getObject()->find(u"tokenModifiers");

            if (tokenTypes_value.type() == variable::ValueType::Array)
                for(auto & v : tokenTypes_value.getArray()->values())
                    if (v.type() == variable::ValueType::String)
                        res.capabilities.semanticTokensProvider.legend.tokenTypes.push_back(v.getString());

            if (tokenModifiers_value.type() == variable::ValueType::Array)
                for(auto & v : tokenModifiers_value.getArray()->values())
                    if (v.type() == variable::ValueType::String)
                        res.capabilities.semanticTokensProvider.legend.tokenModifiers.push_back(v.getString());
        }
    }

    if (completionProvider_value.isObject()) {
        const variable::Value & triggerCharacters_value = completionProvider_value.getObject()->find(u"triggerCharacters");
        const variable::Value & resolveProvider_value   = completionProvider_value.getObject()->find(u"resolveProvider");
        const variable::Value & completionItem_value    = completionProvider_value.getObject()->find(u"completionItem");

        if (triggerCharacters_value.isArray()) {
            const std::shared_ptr<variable::Array> triggerCharacters_array = triggerCharacters_value.getArray();
            for(const variable::Value & v : triggerCharacters_array->values())
                if (v.isString())
                    res.capabilities.completionProvider.triggerCharacters.push_back(v.getString());
        }

        if (resolveProvider_value.isBoolean())
            res.capabilities.completionProvider.resolveProvider = resolveProvider_value.getBool();

        if (completionItem_value.isObject()) {
            const variable::Value & labelDetailsSupport_value = completionItem_value.getObject()->find(u"labelDetailsSupport");
            if (labelDetailsSupport_value.isBoolean())
                res.capabilities.completionProvider.completionItem.labelDetailsSupport = labelDetailsSupport_value.getBool();
        }
    }

    if (simodo_value.type() == variable::ValueType::Object) {
        const variable::Value & runnerProvider_value    = simodo_value.getObject()->find(u"runnerProvider");
        const variable::Value & debugProvider_value     = simodo_value.getObject()->find(u"debugProvider");
        const variable::Value & runnerOptions_value     = simodo_value.getObject()->find(u"runnerOptions");

        if (runnerProvider_value.type() == variable::ValueType::Bool)
            res.simodo.runnerProvider = runnerProvider_value.getBool();
        if (debugProvider_value.type() == variable::ValueType::Bool)
            res.simodo.debugProvider = debugProvider_value.getBool();
        if (runnerOptions_value.isArray()) {
            const std::shared_ptr<variable::Array> runnerOptions_array = runnerOptions_value.getArray();
            for(const variable::Value & opt_value : runnerOptions_array->values())
                if (opt_value.isObject()) {
                    const variable::Value & languageID_value    = opt_value.getObject()->find(u"languageID");
                    const variable::Value & viewName_value     = opt_value.getObject()->find(u"viewName");

                    if (languageID_value.isString() && viewName_value.isString())
                        res.simodo.runnerOptions.push_back({languageID_value.getString(), viewName_value.getString()});
                }
        }
    }

    if (serverInfo_value.type() == variable::ValueType::Object) {
        const variable::Value & name_value      = serverInfo_value.getObject()->find(u"name");
        const variable::Value & version_value   = serverInfo_value.getObject()->find(u"version");

        if (name_value.type() == variable::ValueType::String)
            res.serverInfo.name = name_value.getString();
        if (version_value.type() == variable::ValueType::String)
            res.serverInfo.version = version_value.getString();
    }

    return res;
}

}